﻿
using System;
using System.Linq;
using DotNetNeural.BackPropagation;
using DotNetNeural.Data.Learning;
using DotNetNeural.Metric;
using DotNetNeural.Perceptron.Algorithm.Data;
using DotNetNeural.Utils.Adapters;
namespace DotNetNeural.Perceptron.Algorithm
{
    /// <summary>
    /// Provides basic back-propagation algorithm for multilayer perceptron. Control items (if there are any) from training
    /// set are used for learning-error calculation.
    /// </summary>
    public class BackPropagationAlgorithm : IBackPropagation
    {
        #region Constants

        public const int MaxIterationsCount = 100000;

        #endregion

        #region Properties

        public BackPropagationImpl Utility { get; private set; }

        public IDeviation Deviation { get; private set; }

        public int AllowedIterationsCount { get; private set; }

        public Action<IterationStatus> IterationCallback { get; set; }


        #endregion

        public BackPropagationAlgorithm()
            : this(new DefaultDeviation())
        {
        }

        public BackPropagationAlgorithm(Action<IterationStatus> iterationCallback)
            : this(new DefaultDeviation(), BackPropagationImpl.DefaultLearningSpeed, MaxIterationsCount, iterationCallback)
        {
        }

        public BackPropagationAlgorithm(IDeviation deviation)
            : this(deviation, BackPropagationImpl.DefaultLearningSpeed)
        {
        }

        public BackPropagationAlgorithm(IDeviation deviation, float learningSpeed)
            : this(deviation, learningSpeed, MaxIterationsCount, null)
        {
        }

        public BackPropagationAlgorithm(IDeviation deviation, float learningSpeed, int allowedIterationsCount)
            : this(deviation, learningSpeed, allowedIterationsCount, null)
        {
        }


        public BackPropagationAlgorithm(IDeviation deviation, float learningSpeed, int allowedIterationsCount, Action<IterationStatus> iterationCallback)
        {
            if (deviation == null)
                throw new NullReferenceException("Illegal null-reference deviation");

            Deviation = deviation;
            IterationCallback = iterationCallback;
            AllowedIterationsCount = allowedIterationsCount;
            Utility = new BackPropagationImpl(learningSpeed);
        }

        #region Protected methods

        /// <summary>
        /// Validates that trainingSet and perceptron have valid dimensions of inputs/outputs and returns
        /// result of validation
        /// </summary>
        protected bool ValidateDimensions(IPerceptron perceptron, ITrainingSet trainingSet)
        {
            int inputsCount = perceptron.InputsCount;
            int outputsCount = perceptron.OutputsCount;

            bool valid = inputsCount == trainingSet.InputsCount &&
                outputsCount == trainingSet.OutputsCount;

            if (!valid)
                return false;

            //compare all elements in training set
            foreach (var i in trainingSet.LearningItems)
            {
                bool itemValid = i.Input.Length == inputsCount &&
                    i.Output.Length == outputsCount;

                if (!itemValid)
                    return false;
            }

            return true;
        }

        #endregion

        #region IBackPropagation Members

        public void BackPropagate(IPerceptron perceptron, ITrainingSet trainingSet)
        {
            if (perceptron == null || trainingSet == null)
                throw new NullReferenceException("Illegal null-reference args");

            if (!ValidateDimensions(perceptron, trainingSet))
                throw new OperationCanceledException("Dimensions of training set and perceptron are different");

            float error = Single.MaxValue;
            float allowedError = 0.1f;
            DeviationAdapter deviation = new DeviationAdapter(Deviation);

            int currentIteration = 0;

            while (error > allowedError && currentIteration < AllowedIterationsCount)
            {
                var bpError = CalculateDeviation(perceptron, trainingSet);
                error = bpError.Deviation; // perceptron is learning using learning items in training set

                var iterator = trainingSet.CreateIterator(false);
                while (!iterator.IsLast)
                {
                    Utility.BackPropagate(perceptron, iterator.Value.Input, iterator.Value.Output);
                    iterator.MoveNext();
                }

                if (IterationCallback != null)
                {
                    IterationCallback(new IterationStatus()
                    {
                        Deviation = bpError,
                        IterationIndex = currentIteration
                    });
                }

                ++currentIteration;
            }
        }

        #endregion

        #region Private methods

        private BackPropagationDeviation CalculateDeviation(IPerceptron perceptron, ITrainingSet trainingSet)
        {
            var result = new BackPropagationDeviation()
            {
                Deviation = CalculateDeviationImpl(perceptron, trainingSet, false),
                ControlDeviation = Single.PositiveInfinity
            };

            if (CanUseControlSetItems(trainingSet))
            {
                result.ControlDeviation = CalculateDeviationImpl(perceptron, trainingSet, true);
            }

            return result;
        }

        private bool CanUseControlSetItems(ITrainingSet trainingSet)
        {
            return trainingSet.ControlItems.Count() > 0;
        }

        private float CalculateDeviationImpl(IPerceptron perceptron, ITrainingSet trainingSet, bool useControlItems)
        {
            float result = 0;

            var iterator = trainingSet.CreateIterator(useControlItems);

            while (!iterator.IsLast)
            {
                float[] actualOutput = perceptron.Propagate(iterator.Value.Input);
                float[] expectedOutput = iterator.Value.Output;

                result += Deviation.CalculateDeviation(actualOutput, expectedOutput);

                iterator.MoveNext();
            }

            return result;
        }

        #endregion
    }
}
